package wechat

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"sync"
	"time"
)

type Client struct {
	corpId     string
	httpClient *http.Client

	tokenMapRWMutex sync.RWMutex
	tokensByAgentId map[string]*Agent
	tokensBySecret  map[string]*Agent
}

type Agent struct {
	AgentId string
	Secret  string

	AccessToken        string
	AccessTokenOutTime time.Time
}

// NewWechatClient 初始化微信API客户端
// @param corpId		企业ID
// @param secret		应用Secret
// @param agentId		应用ID
// @param clientTimeout	HTTP超时时间(s)
func NewWechatClient(corpId string, clientTimeout time.Duration) *Client {
	return &Client{
		corpId: corpId,
		httpClient: &http.Client{
			Timeout: clientTimeout * time.Second,
		},
		tokensByAgentId: map[string]*Agent{},
		tokensBySecret:  map[string]*Agent{},
	}
}

func (w *Client) AddAgent(agentId string, secret string) error {
	if w.GetAgentById(agentId) != nil {
		return nil
	}

	w.tokenMapRWMutex.Lock()
	agent := Agent{
		AgentId: agentId,
		Secret:  secret,
	}
	w.tokensByAgentId[agentId] = &agent
	w.tokensBySecret[secret] = &agent
	w.tokenMapRWMutex.Unlock()

	if _, _, err := w.getToken(w.corpId, secret); err != nil {
		return err
	}

	return nil
}

func (w *Client) GetAgentById(agentId string) *Agent {
	w.tokenMapRWMutex.RLock()
	defer w.tokenMapRWMutex.RUnlock()

	return w.tokensByAgentId[agentId]
}

func (w *Client) GetAgentBySecret(secret string) *Agent {
	w.tokenMapRWMutex.RLock()
	defer w.tokenMapRWMutex.RUnlock()

	return w.tokensBySecret[secret]
}

type Token struct {
	ErrCode     int    `json:"errcode"`
	ErrMsg      string `json:"errmsg"`
	AccessToken string `json:"access_token"`
	ExpiresIn   int    `json:"expires_in"`
}

// https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=?&corpsecret=?
// getToken 获取access_token
// @param cropId 		企业ID
// @param corpSecret 	应用Secret
func (w *Client) getToken(corpId, corpSecret string) (accessToken string, expiresIn int, err error) {
	url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s", corpId, corpSecret)
	resp, err := w.httpClient.Get(url)
	if err != nil {
		return "", 0, err
	}

	defer resp.Body.Close()

	readAll, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", 0, err
	}

	var token Token
	if err := json.Unmarshal(readAll, &token); err != nil {
		return "", 0, err
	}

	if token.ErrCode != 0 {
		return "", 0, errors.New(token.ErrMsg)
	}
	agent := w.GetAgentBySecret(corpSecret)
	if agent == nil {
		return "", 0, errors.New("请先使用AddAgent方法")
	}

	agent.AccessToken = token.AccessToken
	agent.AccessTokenOutTime = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)

	return token.AccessToken, token.ExpiresIn, nil
}

type SendResult struct {
	ErrCode     int    `json:"errcode"`
	ErrMsg      string `json:"errmsg"`
	InvalidUser string `json:"invaliduser"`
}

func (s SendResult) Error() string {
	marshal, err := json.Marshal(s)
	if err != nil {
		return err.Error()
	}
	return string(marshal)
}

func (w *Client) SendTextMessage(toUser string, agentId string, content string) (invalidUser string, err error) {
	return w.sendMessage(toUser, agentId, "text", map[string]string{"content": content})
}

func (w *Client) SendMarkdownMessage(toUser string, agentId string, content string) (invalidUser string, err error) {
	return w.sendMessage(toUser, agentId, "markdown", map[string]string{"content": content})
}

func (w *Client) SendFileMessage(toUser string, agentId string, mediaId string) (invalidUser string, err error) {
	return w.sendMessage(toUser, agentId, "file", map[string]string{"media_id": mediaId})
}

func (w *Client) SendImageMessage(toUser string, agentId string, mediaId string) (invalidUser string, err error) {
	return w.sendMessage(toUser, agentId, "image", map[string]string{"media_id": mediaId})
}

// https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=?
// sendTextMessage 发送文本消息
// @param accessToken
// @param corpSecret 	应用Secret
func (w *Client) sendMessage(toUser string, agentId string, msgType string, content interface{}) (invalidUser string, err error) {
	agent := w.GetAgentById(agentId)
	if agent.AccessToken == "" || agent.AccessTokenOutTime.Before(time.Now()) {
		if _, _, err := w.getToken(w.corpId, agent.Secret); err != nil {
			panic(err)
		}
	}
	url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s", agent.AccessToken)

	marshal, _ := json.Marshal(map[string]interface{}{
		msgType:   content,
		"touser":  toUser,
		"msgtype": msgType,
		"agentid": agentId,
		"safe":    0,
	})

	resp, err := w.httpClient.Post(url, "application/json", bytes.NewReader(marshal))
	if err != nil {
		return "", err
	}

	defer resp.Body.Close()
	readAll, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	log.Println("[sendMessage]", string(readAll))

	var sendResult SendResult
	if err := json.Unmarshal(readAll, &sendResult); err != nil {
		return "", err
	}

	if sendResult.ErrCode != 0 {
		return sendResult.InvalidUser, sendResult
	}

	return sendResult.InvalidUser, nil
}

type UploadFile struct {
	ErrCode   int    `json:"errcode"`
	ErrMsg    string `json:"errmsg"`
	Type      string `json:"type"`
	MediaID   string `json:"media_id"`
	CreatedAt string `json:"created_at"`
}

// https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=?&type=?
func (w *Client) MediaUpload(agentId string, media []byte, filename, fileType string) (mediaId string, err error) {
	buffer := new(bytes.Buffer)
	writer := multipart.NewWriter(buffer)
	file, err := writer.CreateFormFile("media", filename)
	if err != nil {
		return "", err
	}

	if _, err := file.Write(media); err != nil {
		return "", err
	}

	if err := writer.Close(); err != nil {
		return "", err
	}
	url := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s", w.tokensByAgentId[agentId].AccessToken, fileType)

	resp, err := w.httpClient.Post(url, writer.FormDataContentType(), buffer)
	if err != nil {
		return "", err
	}

	defer resp.Body.Close()
	readAll, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	log.Println("[mediaUpload]", string(readAll))

	var uploadFile UploadFile
	if err := json.Unmarshal(readAll, &uploadFile); err != nil {
		return "", err
	}

	if uploadFile.ErrCode != 0 {
		return "", errors.New(uploadFile.ErrMsg)
	}

	return uploadFile.MediaID, nil
}
